/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.platform.plugin;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.activation.DataHandler;
import javax.mail.AuthenticationFailedException;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.Node;
import org.pentaho.platform.api.engine.IAcceptsRuntimeInputs;
import org.pentaho.platform.api.repository.IContentItem;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.util.messages.LocaleHelper;
import org.pentaho.reporting.platform.plugin.messages.Messages;
/**
* Creation-Date: 27.09.2009, 00:30:00
*
* @author Pedro Alves - WebDetails
*/
public class SimpleEmailComponent implements IAcceptsRuntimeInputs {
/**
* The logging for logging messages from this component
*/
private static final Log log = LogFactory.getLog( SimpleEmailComponent.class );
private static final String MAILER = "smtpsend"; //$NON-NLS-1$
private Map<String, Object> inputs;
private String outputType;
// Inputs
public static final String INPUT_TO = "to";
public static final String INPUT_FROM = "from";
public static final String INPUT_CC = "cc";
public static final String INPUT_BCC = "bcc";
public static final String INPUT_SUBJECT = "subject";
public static final String INPUT_MESSAGEPLAIN = "message-plain";
public static final String INPUT_MESSAGEHTML = "message-html";
public static final String INPUT_MIMEMESSAGE = "mime-message";
// beans
private String to;
private String from;
private String cc;
private String bcc;
private String subject;
private Object messagePlain;
private Object messageHtml;
private IContentItem mimeMessage;
private String attachmentName;
private IContentItem attachmentContent;
private String attachmentName2;
private IContentItem attachmentContent2;
private String attachmentName3;
/*
* Default constructor
*/
public SimpleEmailComponent() {
}
// ----------------------------------------------------------------------------
// BEGIN BEAN METHODS
// ----------------------------------------------------------------------------
public String getBcc() {
return bcc;
}
public void setBcc( final String bcc ) {
this.bcc = bcc;
}
public String getCc() {
return cc;
}
public void setCc( final String cc ) {
this.cc = cc;
}
public String getFrom() {
return from;
}
public void setFrom( final String from ) {
this.from = from;
}
public String getSubject() {
return subject;
}
public void setSubject( final String subject ) {
this.subject = subject;
}
public String getTo() {
return to;
}
public void setTo( final String to ) {
this.to = to;
}
public Object getMessageHtml() {
return messageHtml;
}
public void setMessageHtml( final Object messageHtml ) {
this.messageHtml = messageHtml;
}
public Object getMessagePlain() {
return messagePlain;
}
public void setMessagePlain( final Object messagePlain ) {
this.messagePlain = messagePlain;
}
public IContentItem getMimeMessage() {
return mimeMessage;
}
public void setMimeMessage( final IContentItem mimeMessage ) {
this.mimeMessage = mimeMessage;
}
public IContentItem getAttachmentContent() {
return attachmentContent;
}
public void setAttachmentContent( final IContentItem attachmentContent ) {
this.attachmentContent = attachmentContent;
}
public String getAttachmentName() {
return attachmentName;
}
public void setAttachmentName( final String attachmentName ) {
this.attachmentName = attachmentName;
}
public IContentItem getAttachmentContent2() {
return attachmentContent2;
}
public void setAttachmentContent2( final IContentItem attachmentContent2 ) {
this.attachmentContent2 = attachmentContent2;
}
public IContentItem getAttachmentContent3() {
return attachmentContent3;
}
public void setAttachmentContent3( final IContentItem attachmentContent3 ) {
this.attachmentContent3 = attachmentContent3;
}
public String getAttachmentName2() {
return attachmentName2;
}
public void setAttachmentName2( final String attachmentName2 ) {
this.attachmentName2 = attachmentName2;
}
public String getAttachmentName3() {
return attachmentName3;
}
public void setAttachmentName3( final String attachmentName3 ) {
this.attachmentName3 = attachmentName3;
}
private IContentItem attachmentContent3;
/**
* Sets the mime-type for determining which report output type to generate. This should be a mime-type for consistency
* with streaming output mime-types.
*
* @param outputType
* the desired output type (mime-type) for the report engine to generate
*/
public void setOutputType( final String outputType ) {
this.outputType = outputType;
}
/**
* Gets the output type, this should be a mime-type for consistency with streaming output mime-types.
*
* @return the current output type for the report
*/
public String getOutputType() {
return outputType;
}
/**
* This method sets the map of *all* the inputs which are available to this component. This allows us to use
* action-sequence inputs as parameters for our reports.
*
* @param inputs
* a Map containing inputs
*/
public void setInputs( final Map<String, Object> inputs ) {
this.inputs = inputs;
if ( inputs.containsKey( INPUT_FROM ) ) {
setFrom( (String) inputs.get( INPUT_FROM ) );
}
if ( inputs.containsKey( INPUT_TO ) ) {
setTo( (String) inputs.get( INPUT_TO ) );
}
if ( inputs.containsKey( INPUT_CC ) ) {
setCc( (String) inputs.get( INPUT_CC ) );
}
if ( inputs.containsKey( INPUT_BCC ) ) {
setBcc( (String) inputs.get( INPUT_BCC ) );
}
if ( inputs.containsKey( INPUT_SUBJECT ) ) {
setSubject( (String) inputs.get( INPUT_SUBJECT ) );
}
if ( inputs.containsKey( INPUT_MESSAGEPLAIN ) ) {
setMessagePlain( (String) inputs.get( INPUT_MESSAGEPLAIN ) );
}
if ( inputs.containsKey( INPUT_MESSAGEHTML ) ) {
setMessageHtml( (String) inputs.get( INPUT_MESSAGEHTML ) );
}
if ( inputs.containsKey( INPUT_MIMEMESSAGE ) ) {
setMimeMessage( (IContentItem) inputs.get( INPUT_MIMEMESSAGE ) );
}
}
// ----------------------------------------------------------------------------
// END BEAN METHODS
// ----------------------------------------------------------------------------
protected Object getInput( final String key, final Object defaultValue ) {
if ( inputs != null ) {
final Object input = inputs.get( key );
if ( input != null ) {
return input;
}
}
return defaultValue;
}
/**
* This method will determine if the component instance 'is valid.' The validate() is called after all of the bean
* 'setters' have been called, so we may validate on the actual values, not just the presence of inputs as we were
* historically accustomed to.
* <p/>
* Since we should have a list of all action-sequence inputs, we can determine if we have sufficient inputs to meet
* the parameter requirements This would include validation of values and ranges of values.
*
* @return true if valid
* @throws Exception
*/
public boolean validate() throws Exception {
boolean result = true;
if ( getTo() == null ) {
log.error( Messages.getInstance().getString( "ReportPlugin.emailToNotProvided" ) ); //$NON-NLS-1$
return false;
} else if ( getFrom() == null ) {
log.error( Messages.getInstance().getString( "ReportPlugin.emailFromNotProvided" ) ); //$NON-NLS-1$
return false;
} else if ( ( getMessagePlain() == null ) && ( getMessageHtml() == null ) && ( getMimeMessage() == null ) ) {
log.error( Messages.getInstance().getString( "ReportPlugin.emailContentNotProvided" ) ); //$NON-NLS-1$
result = false;
}
return result;
}
/**
* Perform the primary function of this component, this is, to execute. This method will be invoked immediately
* following a successful validate().
* <p/>
* This method has 2 ways of working:
* <p/>
* 1. You supply a mimeMessage: That mimeMessage will be sent; Optionally, contents will be added as attachment and
* the original mimeMessage will be encapsulated under a multipart/mixed
* <p/>
* <p/>
* 2. You supply a messageHtml and/or a messageText. A new mimemessage will be built. If you supply both, a
* multipart/alternative will be used. After that attachments will be included
*
* @return true if successful execution
* @throws Exception
*/
public boolean execute() throws Exception {
try {
// Get the session object
final Session session = buildSession();
// Create the message
final MimeMessage msg = new MimeMessage( session );
// From, to, etc.
applyMessageHeaders( msg );
// Get main message multipart
final Multipart multipartBody = getMultipartBody( session );
// Process attachments
final Multipart mainMultiPart = processAttachments( multipartBody );
msg.setContent( mainMultiPart );
// Send it
msg.setHeader( "X-Mailer", MAILER ); //$NON-NLS-1$
msg.setSentDate( new Date() );
Transport.send( msg );
return true;
} catch ( SendFailedException e ) {
log.error( Messages.getInstance().getString( "ReportPlugin.emailSendFailed" ) ); //$NON-NLS-1$
} catch ( AuthenticationFailedException e ) {
log.error( Messages.getInstance().getString( "ReportPlugin.emailAuthenticationFailed" ) ); //$NON-NLS-1$
}
return false;
}
private Multipart getMultipartBody( final Session session ) throws MessagingException, IOException {
// if we have a mimeMessage, use it. Otherwise, build one with what we have
// We can have both a messageHtml and messageText. Build according to it
MimeMultipart parentMultipart = new MimeMultipart();
MimeBodyPart htmlBodyPart = null, textBodyPart = null;
if ( getMimeMessage() != null ) {
// Rebuild a MimeMessage and use this one
final MimeBodyPart original = new MimeBodyPart();
final MimeMessage originalMimeMessage = new MimeMessage( session, getMimeMessage().getInputStream() );
final MimeMultipart relatedMultipart = (MimeMultipart) originalMimeMessage.getContent();
parentMultipart = relatedMultipart;
htmlBodyPart = new MimeBodyPart();
htmlBodyPart.setContent( relatedMultipart );
}
// The information we have in the mime-message overrides the getMessageHtml.
if ( getMessageHtml() != null && htmlBodyPart != null ) {
final String content = getInputString( getMessageHtml() );
htmlBodyPart = new MimeBodyPart();
htmlBodyPart.setContent( content, "text/html; charset=" + LocaleHelper.getSystemEncoding() );
final MimeMultipart htmlMultipart = new MimeMultipart();
htmlMultipart.addBodyPart( htmlBodyPart );
parentMultipart = htmlMultipart;
}
if ( getMessagePlain() != null ) {
final String content = getInputString( getMessagePlain() );
textBodyPart = new MimeBodyPart();
textBodyPart.setContent( content, "text/plain; charset=" + LocaleHelper.getSystemEncoding() );
final MimeMultipart textMultipart = new MimeMultipart();
textMultipart.addBodyPart( textBodyPart );
parentMultipart = textMultipart;
}
// We have both text and html? Encapsulate it in a multipart/alternative
if ( htmlBodyPart != null && textBodyPart != null ) {
final MimeMultipart alternative = new MimeMultipart( "alternative" );
alternative.addBodyPart( textBodyPart );
alternative.addBodyPart( htmlBodyPart );
parentMultipart = alternative;
}
return parentMultipart;
}
private String getInputString( final Object param ) throws IOException {
if ( param instanceof String ) {
return (String) param;
} else if ( param instanceof IContentItem ) {
final InputStream in = ( (IContentItem) param ).getInputStream();
// Convert to String
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int nextChar;
while ( ( nextChar = in.read() ) != -1 ) {
out.write( nextChar );
}
return new String( out.toString( LocaleHelper.getSystemEncoding() ) );
}
throw new IllegalStateException( "Input is not a String or ContentItem" );
}
private Multipart processAttachments( final Multipart multipartBody ) throws MessagingException, IOException {
if ( getAttachmentContent() == null ) {
// We don't have a first attachment, won't even search for the others.
return multipartBody;
}
// We have attachments; Creating a multipart-mixed
final MimeMultipart mixedMultipart = new MimeMultipart( "mixed" );
// Add the first part
final MimeBodyPart bodyPart = new MimeBodyPart();
bodyPart.setContent( multipartBody );
mixedMultipart.addBodyPart( bodyPart );
// Process each of the attachments we have
processSpecificAttachment( mixedMultipart, getAttachmentContent() );
processSpecificAttachment( mixedMultipart, getAttachmentContent2() );
processSpecificAttachment( mixedMultipart, getAttachmentContent3() );
return mixedMultipart;
}
private void processSpecificAttachment( final MimeMultipart mixedMultipart, final IContentItem attachmentContent )
throws IOException, MessagingException {
// Add this attachment
if ( attachmentContent != null ) {
final ByteArrayDataSource dataSource =
new ByteArrayDataSource( attachmentContent.getInputStream(), attachmentContent.getMimeType() );
final MimeBodyPart attachmentBodyPart = new MimeBodyPart();
attachmentBodyPart.setDataHandler( new DataHandler( dataSource ) );
attachmentBodyPart.setFileName( getAttachmentName() );
mixedMultipart.addBodyPart( attachmentBodyPart );
}
}
private void applyMessageHeaders( final MimeMessage msg ) throws Exception {
msg.setFrom( new InternetAddress( from ) );
msg.setRecipients( Message.RecipientType.TO, InternetAddress.parse( to, false ) );
if ( ( cc != null ) && ( cc.trim().length() > 0 ) ) {
msg.setRecipients( Message.RecipientType.CC, InternetAddress.parse( cc, false ) );
}
if ( ( bcc != null ) && ( bcc.trim().length() > 0 ) ) {
msg.setRecipients( Message.RecipientType.BCC, InternetAddress.parse( bcc, false ) );
}
if ( subject != null ) {
msg.setSubject( subject, LocaleHelper.getSystemEncoding() );
}
}
private Session buildSession() throws Exception {
final Properties props = new Properties();
try {
final Document configDocument =
PentahoSystem.getSystemSettings().getSystemSettingsDocument( "smtp-email/email_config.xml" ); //$NON-NLS-1$
final List properties = configDocument.selectNodes( "/email-smtp/properties/*" ); //$NON-NLS-1$
final Iterator propertyIterator = properties.iterator();
while ( propertyIterator.hasNext() ) {
final Node propertyNode = (Node) propertyIterator.next();
final String propertyName = propertyNode.getName();
final String propertyValue = propertyNode.getText();
props.put( propertyName, propertyValue );
}
} catch ( Exception e ) {
log.error( Messages.getInstance().getString( "ReportPlugin.emailConfigFileInvalid" ) ); //$NON-NLS-1$
throw e;
}
final boolean authenticate = "true".equals( props.getProperty( "mail.smtp.auth" ) ); //$NON-NLS-1$//$NON-NLS-2$
// Get a Session object
final Session session;
if ( authenticate ) {
final Authenticator authenticator = new EmailAuthenticator();
session = Session.getInstance( props, authenticator );
} else {
session = Session.getInstance( props );
}
// if debugging is not set in the email config file, match the
// component debug setting
if ( !props.containsKey( "mail.debug" ) ) { //$NON-NLS-1$
session.setDebug( true );
}
return session;
}
/**
* This method returns the output-type for the streaming output, it is the same as what is returned by getOutputType()
* for consistency.
*
* @return the mime-type for the streaming output
*/
public String getMimeType() {
return outputType;
}
private static class EmailAuthenticator extends Authenticator {
private EmailAuthenticator() {
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
final String user = PentahoSystem.getSystemSetting( "smtp-email/email_config.xml", "mail.userid", null ); //$NON-NLS-1$ //$NON-NLS-2$
final String password = PentahoSystem.getSystemSetting( "smtp-email/email_config.xml", "mail.password", null ); //$NON-NLS-1$ //$NON-NLS-2$
return new PasswordAuthentication( user, password );
}
}
}